iT邦幫忙

0

VScode 開發應用系統專案(5-2) - Spring Boot 排程作業(Schedule)

  • 分享至 

  • xImage
  •  

Spring Boot 排程作業(Schedule)控制

概述

** 遇到發文存檔失敗,所以分兩部分發文,控制檔案SysScheduleCtlEntity 以及排程執行紀錄檔案SysScheduleLogEntity。請參考(5-1) - Spring Boot 排程作業(Schedule) URL: https://ithelp.ithome.com.tw/articles/10398729

這裡準備提供Schedule Job 功能設定,基礎控制與紀錄功能等架構,方便後續增加任何排程,只需要簡單增加一個Worker繼承基本BaseScheduleTask,實作triggerScheduleTask 設定排程時間週期規則,以及doTaskWorker實際排程需要執行的功能。

ScheduleTask,提供排程基礎架構與服務

新增ScheduleTaskService 於 tw.lewishome.webapp.base.schedule package(如package不存在,請新增)

** 主要提供功能:

  • 判斷目前主機是否為排程Master主機 (masterServer)
  • 判斷指定的排程任務是否正在執行中
  • 新增 SysScheduleLog
  • 系統紀錄結束 SysScheduleLog
  • 新增 SysScheduleCtl Lock 資料 for Task Name
  • 刪除 SysScheduleLock by Task Name (確認 Task 要執行)
  • 解除所有排程任務鎖定
  • 鎖定所有排程任務
  • 列出所有已註冊的 Scheduled Task Jobs
package tw.lewishome.webapp.base.schedule;

import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor;
import org.springframework.scheduling.config.CronTask;
import org.springframework.scheduling.config.FixedDelayTask;
import org.springframework.scheduling.config.FixedRateTask;
import org.springframework.scheduling.config.ScheduledTask;
import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.utility.common.CommUtils;
import tw.lewishome.webapp.base.utility.common.NetUtils;
import tw.lewishome.webapp.database.primary.entity.SysScheduleCtlEntity;
import tw.lewishome.webapp.database.primary.entity.SysScheduleLogEntity;
import tw.lewishome.webapp.database.primary.repository.SysScheduleCtlRepository;
import tw.lewishome.webapp.database.primary.repository.SysScheduleLogRepository;

/**
 * 排程任務服務類別,負責管理排程任務的執行狀態、控制資料及日誌記錄。
 *
 * 此服務提供以下功能:
 * <ul>
 * <li>判斷指定排程任務是否正在執行中,並根據控制資料與 Lock 資料進行判斷。</li>
 * <li>首次執行時自動新增控制資料與 Lock 資料,確保主機啟動排程任務。</li>
 * <li>根據控制資料設定,判斷是否允許執行排程任務,並檢查主機名稱是否符合。</li>
 * <li>新增與結束排程任務執行日誌,方便追蹤任務執行狀態。</li>
 * <li>新增與刪除排程任務 Lock 資料,確保同一時間僅有一臺主機執行任務,避免多主機衝突。</li>
 * </ul>
 *
 * <b>注意事項:</b>
 * <ul>
 * <li>多臺主機同時啟動排程任務時,可能會因 Lock 資料未及時刪除而產生衝突,建議排程任務執行結束時務必刪除 Lock 資料。</li>
 * <li>控制資料中的 RunFlag 與 ServerName 設定將影響排程任務是否允許執行及執行主機。</li>
 * <li>所有資料操作皆透過 Repository 進行,確保資料一致性。</li>
 * </ul>
 *
 * @author Lewis
 * @since 2024
 */
@Service
@Slf4j
public class ScheduleTaskService {

    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * Constructs a new ScheduleTaskService instance.
     * This is the default constructor, implicitly provided by the compiler
     * if no other constructors are defined.
     */
    public ScheduleTaskService() {
        // Constructor body (can be empty)
    }

    @Autowired
    ApplicationContext applicationContext;

    @Autowired
    SysScheduleLogRepository sysScheduleLogRepository;

    @Autowired
    SysScheduleCtlRepository sysScheduleCtlRepository;

    /**
     * 判斷目前主機是否為排程Master主機 (masterServer)
     *
     * 此方法會檢查 SysScheduleCtl 資料表中的 Main_Server 記錄,
     * 若該記錄的 ServerName 與目前主機名稱相符,則視為排程主機。
     * 若該記錄不存在,則會新增一筆記錄,並將目前主機名稱設為排程主機。
     *
     * @param serverName 目前主機名稱
     * @return 若為排程主機則回傳 true,否則回傳 false
     */
    public Boolean isScheduleMainServer(String serverName) {
        String mainServerName = NetUtils.getHostName().trim();
        SysScheduleCtlEntity sysScheduleCtlEntityMainServer = new SysScheduleCtlEntity();
        SysScheduleCtlEntity.DataKey dataKeyMainServer = new SysScheduleCtlEntity.DataKey();
        dataKeyMainServer.setScheduleTaskName("Main_Server");
        dataKeyMainServer.setDataType("masterServer");
        sysScheduleCtlEntityMainServer = sysScheduleCtlRepository.findById(dataKeyMainServer).orElse(null);
        // 取得 masterServer 名稱
        if (sysScheduleCtlEntityMainServer != null) {
            mainServerName = sysScheduleCtlEntityMainServer.getServerName().trim();
            // 比對是否為 masterServer
            if (StringUtils.equalsIgnoreCase(mainServerName, serverName)) {
                return true;
            }
        } else { // 沒有設定 masterServer,新增一筆資料
            sysScheduleCtlEntityMainServer = new SysScheduleCtlEntity();
            SysScheduleCtlEntity.DataKey dataKeyMainServerNew = new SysScheduleCtlEntity.DataKey();
            dataKeyMainServerNew.setScheduleTaskName("Main_Server");
            dataKeyMainServerNew.setDataType("masterServer");
            sysScheduleCtlEntityMainServer.setDataKey(dataKeyMainServerNew);
            sysScheduleCtlEntityMainServer.setServerName(serverName);
            sysScheduleCtlRepository.saveAndFlush(sysScheduleCtlEntityMainServer);
            return true;
        }
        // 不是 masterServer
        return false;

    }

    /**
     * 判斷指定的排程任務是否正在執行中。
     *
     * 此方法會根據排程任務名稱,檢查控制資料與 Lock 資料,判斷任務是否執行中。
     * 若為首次執行,會主要主機(Main Server)自動新增控制資料
     * 若控制資料主機發現沒有 Lock 資料自動新增 (停機時未處理好) 。
     * 若控制資料設定不執行或指定主機名稱與目前主機名稱不符,則視為正在執行中。
     *
     *
     * @param scheduleTaskName 排程任務名稱
     * @return 若排程任務正在執行或不允許執行,則回傳 true 執行中(不允許);否則回傳 false 可執行
     */

    public Boolean isScheduleTaskRunning(String scheduleTaskName) {
        String currentServer = NetUtils.getHostName().trim();
        // 取得 sysSchedule control 控制資料
        SysScheduleCtlEntity.DataKey dataKeyCtl = new SysScheduleCtlEntity.DataKey();
        dataKeyCtl.setScheduleTaskName(scheduleTaskName);
        dataKeyCtl.setDataType("controller");
        SysScheduleCtlEntity sysScheduleCtlEntity = sysScheduleCtlRepository.findById(dataKeyCtl).orElse(null);
        // 沒有 sysSchedule control 控制資料,代表第一次執行,需要新增一筆 控制資料以及 Lock資料 (認定 master server
        // 主機啟動執行)
        if (sysScheduleCtlEntity == null) {
            log.info(scheduleTaskName + " First time trigger {} sysScheduleCtl has no record)", scheduleTaskName);
            // 新增 scheduleTaskName controller 資料
            sysScheduleCtlEntity = new SysScheduleCtlEntity();
            dataKeyCtl = new SysScheduleCtlEntity.DataKey();
            dataKeyCtl.setScheduleTaskName(scheduleTaskName);
            dataKeyCtl.setDataType("controller");
            sysScheduleCtlEntity.setDataKey(dataKeyCtl);
            sysScheduleCtlEntity.setServerName(currentServer);
            sysScheduleCtlRepository.saveAndFlush(sysScheduleCtlEntity);
            addSysScheduleLog(scheduleTaskName);
            // 第一次新增 Controller ,暫不執行,等待下次執行時刪除 Lock 資料 。
            return true;
        }        
        String sysScheduleCtlServer = sysScheduleCtlEntity.getServerName().trim();
        if (sysScheduleCtlEntity.getRunFlag() != null && Boolean.FALSE.equals(sysScheduleCtlEntity.getRunFlag())) {
            // Schedule RunFlag設定不執行。
            log.info(scheduleTaskName + " sysScheduleCtl running flag is false (not Running)");
            // 回傳 執行中
            return true;
        }

        // sysScheduleCtl 沒有指定主機(空白), 可以多臺主機執行,刪除Lock確認可否執行 。
        if (StringUtils.isBlank(sysScheduleCtlServer)) {
            // 刪除Lock失敗,表示 執行中
            if (Boolean.FALSE.equals(delSysScheduleLock(scheduleTaskName))) {
                log.info(scheduleTaskName + " Do delete lock Failed (schedule should be running)");
                return true;
            }
            // 刪除 Lock 成功,表示沒有執行中
            return false;
        } else { // sysScheduleCtl 指定主機(非空白), 比對主機名稱是否相符
            if (StringUtils.equalsIgnoreCase(sysScheduleCtlServer, currentServer)) {
                log.info(currentServer + " is " + scheduleTaskName + " Register server ");
                // 是指定主機,不會重複執行(就算執行時間超過週期), 所以一律刪除lock (不管成功否)
                delSysScheduleLock(scheduleTaskName);
                // 回傳 沒有執行中。
                return false;
            }
        }
        return true;

    }

    /**
     * 新增 SysScheduleLog
     *
     * @param scheduleTaskName task Name
     * @return String SysScheduleLog Uuid
     */
    public String addSysScheduleLog(String scheduleTaskName) {
        SysScheduleLogEntity oneSysScheduleLogEntity = new SysScheduleLogEntity();
        SysScheduleLogEntity.DataKey dataKey = new SysScheduleLogEntity.DataKey();
        oneSysScheduleLogEntity.setDataKey(dataKey);
        oneSysScheduleLogEntity.setScheduleTaskName(scheduleTaskName);
        oneSysScheduleLogEntity.setServerName(NetUtils.getHostName());

        sysScheduleLogRepository.saveAndFlush(oneSysScheduleLogEntity);
        return dataKey.getUuid();
    }

    /**
     * 系統紀錄結束 SysScheduleLog
     *
     * @param scheduleLogUUid scheduleUUid
     */
    public void endSysScheduleLog(String scheduleLogUUid) {
        try {
            SysScheduleLogEntity.DataKey dataKey = new SysScheduleLogEntity.DataKey();
            dataKey.setUuid(scheduleLogUUid);

            Optional<SysScheduleLogEntity> optionalSysScheduleLogEntity = sysScheduleLogRepository.findById(dataKey);
            if (optionalSysScheduleLogEntity.isPresent()) {
                SysScheduleLogEntity oneSysScheduleLogEntity = optionalSysScheduleLogEntity.get();
                sysScheduleLogRepository.saveAndFlush(oneSysScheduleLogEntity);
            }
        } catch (Exception ex) {
            log.error("SysScheduleLog {} not Found", scheduleLogUUid);
        }

    }

    /**
     * 新增 SysScheduleCtl Lock 資料 for Task Name
     *
     * @param scheduleTaskName task Name
     */
    public void addSysScheduleCtlLock(String scheduleTaskName) throws RuntimeException {
        SysScheduleCtlEntity sysScheduleCtlEntity = new SysScheduleCtlEntity();
        SysScheduleCtlEntity.DataKey dataKey = new SysScheduleCtlEntity.DataKey();
        dataKey.setScheduleTaskName(scheduleTaskName.trim());
        dataKey.setDataType("taskLock");
        sysScheduleCtlEntity.setDataKey(dataKey);
        sysScheduleCtlEntity.setServerName(NetUtils.getHostName());
        sysScheduleCtlEntity.setRunFlag(true);
        try {
            sysScheduleCtlRepository.saveAndFlush(sysScheduleCtlEntity);
        } catch (Exception ex) {
            throw new RuntimeException(" Add  " + scheduleTaskName + " Lock Record Fail");
        }
    }

    /**
     * 刪除 SysScheduleLock by Task Name (確認 Task 要執行)
     *
     * @return Boolean isRunning
     * @param scheduleTaskName a String object
     */
    public Boolean delSysScheduleLock(String scheduleTaskName) {
        try {
            SysScheduleCtlEntity.DataKey dataKey = new SysScheduleCtlEntity.DataKey();
            dataKey.setScheduleTaskName(scheduleTaskName.trim());
            dataKey.setDataType("taskLock");
            sysScheduleCtlRepository.deleteById(dataKey);
        } catch (Exception ex) {
            return false;
        }
        return true;
    }

    /**
     * 
     * 解除所有排程任務鎖定
     * 
     */
    public void releaseAllScheduleTaskLocks() {
        try {
            List<String> listAllScheduleTasks = listScheduledTaskJobs();
            for (String oneScheduleTasks : listAllScheduleTasks) {
                addSysScheduleCtlLock(oneScheduleTasks.trim());
            }
        } catch (Exception ex) {
            log.error("Failed to release all schedule task locks: " + ex.getMessage());
        }

    }

    /**
     * 
     * 鎖定所有排程任務
     * 
     */
    public void lockAllScheduleTasks() {
        try {
            sysScheduleCtlRepository.deleteByDataType("taskLock");
            log.info("Released all schedule task locks, total released ");
        } catch (Exception ex) {
            log.error("Failed to release all schedule task locks: " + ex.getMessage());
        }

    }

    /**
     * 列出所有已註冊的 Scheduled Task Jobs
     * 
     * @return a List object
     */

    public List<String> listScheduledTaskJobs() {
        List<String> listScheduleJobs = new java.util.ArrayList<>();
        ScheduledAnnotationBeanPostProcessor postProcessor = applicationContext
                .getBean(ScheduledAnnotationBeanPostProcessor.class);

        Set<ScheduledTask> scheduledTasks = postProcessor.getScheduledTasks() == null ? Set.of()
                : postProcessor.getScheduledTasks();

        log.info("--- Listing Scheduled Jobs ---");
        if (scheduledTasks.isEmpty()) {
            log.info("No scheduled jobs found.");
            return listScheduleJobs;
        }

        for (ScheduledTask scheduledTask : scheduledTasks) {
            Object task = scheduledTask.getTask();
            String taskString = task.toString();
            List<String> splitTaskString = CommUtils.splitDelimiter(taskString, ".");
            if (splitTaskString.size() > 1) {
                String methodName = splitTaskString.get(splitTaskString.size() - 2);
                log.info(" Method: " + methodName);
                listScheduleJobs.add(methodName.trim());
            } else {
                log.info(" Method: " + taskString);
                listScheduleJobs.add(taskString.trim());
            }

            log.info(" Task: " + task.toString());

            if (task instanceof CronTask cronTask) {
                log.info("    Type: Cron");
                log.info("    Cron Expression: " + cronTask.getExpression());
            } else if (task instanceof FixedRateTask fixedRateTask) {
                log.info("    Type: Fixed Rate");
                log.info("    Fixed Rate: " + fixedRateTask.getIntervalDuration().toMillis() + " ms");
                log.info("    Initial Delay: " + fixedRateTask.getIntervalDuration().toMillis() + " ms");
            } else if (task instanceof FixedDelayTask fixedDelayTask) {
                log.info("    Type: Fixed Delay");
                log.info("    Fixed Delay: " + fixedDelayTask.getIntervalDuration().toMillis() + " ms");
                log.info("    Initial Delay: " + fixedDelayTask.getIntervalDuration().toMillis() + " ms");
            } else {
                log.info("    Type: Unknown");
            }
            log.info("----------------------------");
        }
        return listScheduleJobs;
    }

}

新增ScheduleConfiguration 於 tw.lewishome.webapp.base.schedule package

** 主要提供功能:

  • 設定排程任務執行緒池並處理排程任務鎖定
  • 建立排程任務執行緒池
  • 於關機時自動關閉排程任務執行緒池
package tw.lewishome.webapp.base.schedule;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;
import tw.lewishome.webapp.base.utility.common.NetUtils;

/**
 *
 * 排程組態類別,負責設定應用程式的排程任務執行緒池與排程任務鎖定機制。
 *
 *
 *
 * 此類別於應用程式啟動時執行,根據系統參數 <code>MAIN_SERVER_NAME</code> 進行主機排程任務鎖定的初始化:
 * <ul>
 * <li>若未設定 <code>MAIN_SERVER_NAME</code> 。</li>
 * <li>若主機為 <code>MAIN_SERVER</code>,則解除所有排程任務鎖定。</li>
 * <li>否則僅顯示主機資訊。</li>
 * </ul>
 *
 *
 *
 * 此類別同時建立一個具備 100 條執行緒的排程任務執行緒池,並於應用程式關閉時自動釋放資源。
 *
 *
 *
 * 主要功能:
 * <ul>
 * <li>設定排程任務的執行緒池。</li>
 * <li>根據系統參數進行主機排程任務鎖定的初始化。</li>
 * <li>確保排程任務執行緒池於關機時自動關閉。</li>
 * </ul>
 *
 *
 *
 * 注意事項:
 * <ul>
 * <li>請確保 <code>GetSysParmValueService</code> 已正確注入,否則排程初始化可能失敗。</li>
 * <li>執行緒池大小可依實際需求調整,預設為 100 條執行緒。</li>
 * </ul>
 *
 *
 * @author Lewis
 * @since 2024
 */
@Configuration
@EnableScheduling
@Slf4j
public class ScheduleConfiguration implements SchedulingConfigurer {

    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * Constructs a new ScheduleConfiguration instance.
     * This is the default constructor, implicitly provided by the compiler
     * if no other constructors are defined.
     * 
     */
    public ScheduleConfiguration() {
        // Constructor body (can be empty)
    }

    @Autowired
    private ScheduleTaskService scheduleTaskService;

    private static final String HOST_NAME = StringUtils.defaultIfBlank(NetUtils.getHostName(), "UNKNOWN_HOST");

    // @Autowired
    // private ScheduleTaskService scheduleTaskService;

    /**
     * 
     *
     *
     * 設定排程任務執行緒池並處理排程任務鎖定。
     *
     *
     * 此方法於 Spring 啟動時自動執行,負責:
     * <ul>
     * <li>將排程任務註冊器的執行緒池設為自訂的 100 條執行緒池。</li>
     * <li>根據系統參數 <code>MAIN_SERVER_NAME</code> 決定是否新增或解除排程任務鎖定。</li>
     * </ul>
     *
     *
     * 執行流程:
     *  <ul>
     * <li>取得主機名稱與系統參數 <code>MAIN_SERVER_NAME</code>。</li>
     * <li>若參數未設定,則新增並解除所有排程任務鎖定。</li>
     * <li>若主機為 <code>MAIN_SERVER</code>,則解除所有排程任務鎖定。</li>
     * <li>否則僅顯示主機資訊。</li>
     *  </ul>
     *
     */
    @Override
    public void configureTasks(@NonNull ScheduledTaskRegistrar taskRegistrar) {
        log.info("Schedule Configure Tasks Started");
        taskRegistrar.setScheduler(taskExecutor());
        // 檢查 MAIN_SERVER_NAME 是否有設定 
        if (scheduleTaskService.isScheduleMainServer(HOST_NAME)) {
            // 是 MAIN_SERVER,解除所有排程任務鎖定
            log.info("This Host [{}] is MAIN_SERVER, Release all schedule task locks.", HOST_NAME);
            scheduleTaskService.lockAllScheduleTasks();
            return;
        } else {
            // 不是 MAIN_SERVER,僅顯示主機資訊
            log.info("This Host [{}] is not MAIN_SERVER, No action taken for schedule task locks.", HOST_NAME);
        }
        
    }

    /**
     *
     * 建立排程任務執行緒池。
     *
     *
     * 此方法會建立一個具備 5 條執行緒的排程任務執行緒池,供排程任務使用。
     *
     *
     * @return ThreadPoolTaskScheduler 排程任務執行緒池
     */
    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5); // Set the desired pool size
        scheduler.setThreadNamePrefix("my-scheduled-task-"); // Prefix for thread names
        scheduler.setAwaitTerminationSeconds(60); // Wait up to 60 seconds for tasks to complete on shutdown
        scheduler.setWaitForTasksToCompleteOnShutdown(true); // Wait for tasks to complete
        return scheduler;
    }

    /**
     *
     * 建立排程任務執行緒池,於關機時自動關閉。
     *
     *
     * 此方法會建立一個具備 100 條執行緒的排程任務執行緒池,並於 Spring 容器關閉時自動釋放資源。
     *
     *
     * @return Executor 排程任務執行緒池,供排程任務使用
     */
    @Bean(destroyMethod = "shutdown")
    public Executor taskExecutor() {
        return Executors.newScheduledThreadPool(100);
    }
}

新增BaseScheduleTask 於 tw.lewishome.webapp.base.schedule package

** 主要提供功能:

  • 執行排程任務的主要方法(共用)
  • 指定實作 triggerScheduleTask 方法,定義排程任務的觸發邏輯與執行週期。
  • 指定實作 doTaskWorker 執行排程任務的工作方法。
package tw.lewishome.webapp.base.schedule;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 抽象基礎排程任務類別,提供排程任務執行的標準流程與相關操作方法。
 *
 * 此類別作為所有排程任務的基礎,統一管理排程啟動、執行狀態檢查、日誌記錄等功能,
 * 並要求子類別實作具體的排程邏輯與任務內容。
 *
 * <ul>
 * <li>自動注入
 * {@link tw.lewishome.webapp.base.schedule.ScheduleTaskService},用於排程任務的狀態管理與日誌記錄。</li>
 * <li>提供 {@link #doScheduleTask(Class)} 方法,依據系統參數判斷排程是否啟動,
 * 並確保同一任務不重複執行,執行過程中會記錄開始與結束日誌。</li>
 * <li>定義 {@link #triggerScheduleTask()} 與 {@link #doTaskWorker()} 抽象方法,
 * 強制子類別根據業務需求實作排程觸發邏輯與具體任務內容。</li>
 * </ul>
 *
 * 使用方式:<br>
 * 請繼承此類別並實作 {@link #triggerScheduleTask()} 及 {@link #doTaskWorker()} 方法,
 * 以定義排程任務的觸發條件、執行週期及具體執行內容。
 *
 *
 * @author Lewis
 * @since 2024
 */
@Service
public abstract class BaseScheduleTask {
    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * Constructs a new BaseScheduleTask instance.
     * This is the default constructor, implicitly provided by the compiler
     * if no other constructors are defined.
     */
    public BaseScheduleTask() {
        // Constructor body (can be empty)
    }

    @Autowired
    ScheduleTaskService scheduleTaskService;

    // @Autowired
    // GetSysParmValueService GetSysParmValueService;

    /**
     * 執行排程任務的主要方法。
     *
     * 此方法會依據系統參數判斷排程是否啟動,並檢查任務是否正在執行,
     * 若未執行則開始執行任務並記錄執行日誌。
     *
     *
     * @param taskClass 排程任務的類別,用於取得任務名稱
     */
    @SuppressWarnings("rawtypes")
    public void doScheduleTask(Class taskClass) {
        String scheduleTaskName = taskClass.getSimpleName();

        // 檢查 Schedule Task是否執行中,(多主機,或 Task Over flow run Times)
        // checkScheduleRunning 回傳 true,表示正在執行相同作業,
        if (Boolean.FALSE.equals(scheduleTaskService.isScheduleTaskRunning(scheduleTaskName))) {
            // System.out.println(scheduleTaskName + " is Start");
            String scheduleUUid = scheduleTaskService.addSysScheduleLog(scheduleTaskName);
            doTaskWorker();
            scheduleTaskService.endSysScheduleLog(scheduleUUid);
            scheduleTaskService.addSysScheduleCtlLock(scheduleTaskName);
            // System.out.println(scheduleTaskName + " is End");
        }
    }

    /**
     * 指定實作 triggerScheduleTask 方法,定義排程任務的觸發邏輯與執行週期。
     *
     * 子類別需根據業務需求實作此方法,設定排程觸發條件與週期。
     *
     */
    public abstract void triggerScheduleTask();

    /**
     * 指定實作 doTaskWorker 執行排程任務的工作方法。
     *
     * 子類別需實作此方法以定義具體的排程任務內容。
     *
     */
    public abstract void doTaskWorker();

}

ScheduleTaskWorker,實際排程任務

新增 tw.lewishome.webapp.base.schedule.worker package

** 新增 ScheduleTaskWorkerNoEntityManagerSample 範例 於 worker Package

主要展示新增排程任務,

  • triggerScheduleTasks程式無須調整(自動以Class名稱註冊排程),依說明@Scheduled即可。
  • doTaskWorker程式,於 Try {} 端落實做排程任務

** 以下程式內註解,誤認為垃圾廣告。參照ScheduleTaskWorkerPrimarySample,請自行加入 worker 的 triggerScheduleTasks 的註解

  • @Scheduled的設定方案:
  • @Scheduled(fixedDelay = 5000) 表示當前方法執行完畢5000ms後,Spring scheduling會再次呼叫該方法
  • @Scheduled(fixedRate = 5000)表示當前方法開始執行5000ms後,Springscheduling會再次呼叫該方法
  • @Scheduled(initialDelay = 1000, fixedRate = 5000) 表示延遲1000ms執行第一次任務
  • cron 接受cron 表示式,根據cron表示式確定定時規則
  • @Scheduled(cron = "0 0/10 * * * ?") // 每10分鐘
  • second, minute, hour, day of month, month, day(s) of week
package tw.lewishome.webapp.base.schedule.worker;

import java.util.concurrent.TimeUnit;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.schedule.BaseScheduleTask;

/**
 *
 * ScheduleTaskWorkerNoEntityManagerSample 是一個排程任務範例,繼承自
 * {@link tw.lewishome.webapp.base.schedule.BaseScheduleTask},
 * 主要展示如何在不使用 EntityManager 的情境下執行排程任務。
 *
 *
 *
 * 本類別包含多種排程註解(@Scheduled)的設定範例,說明如何透過 fixedDelay、fixedRate、initialDelay 及 cron
 * 表達式
 * 來控制任務執行的時間間隔與規則。實際啟用的是 {@code triggerScheduleTask()} 方法,使用 cron 表達式每分鐘執行一次。
 *
 *
 *
 * 主要方法說明:
 * <ul>
 * <li>{@link #triggerScheduleTask()}:每分鐘觸發一次排程任務,並呼叫
 * {@link #doScheduleTask(Class)}
 * 方法執行任務邏輯。</li>
 * <li>{@link #doTaskWorker()}:模擬任務執行流程,包含 10 秒的暫停(sleep),可用於測試排程任務的執行狀態。</li>
 * </ul>
 *
 *
 *
 * 注意事項:
 * <ul>
 * <li>本範例不涉及資料庫操作,適合用於純計算或外部 API 呼叫等無需 EntityManager 的排程任務。</li>
 * <li>如需調整排程頻率,請修改 @Scheduled 註解的參數。</li>
 * </ul>
 *
 *
 * @author Lewis
 * @since 2024
 */
@Service
@Slf4j
public class ScheduleTaskWorkerNoEntityManagerSample extends BaseScheduleTask {
    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * Constructs a new ScheduleTaskWorkerNoEntityManagerSample instance.
     * This is the default constructor, implicitly provided by the compiler
     * if no other constructors are defined.
     */
    public ScheduleTaskWorkerNoEntityManagerSample() {
        // Constructor body (can be empty)
    }

    // 自行加入 @Scheduled 註解 
    
    /**
     *
     * triggerScheduleTask.
     *
     */
    @Scheduled(cron = "0 0/1 * * * ?") // 每1分鐘執行
    public void triggerScheduleTask() {
        // Class for Schedule Task
        Class<?> className = this.getClass();
        String scheduleTaskName = className.getSimpleName();
        log.info(scheduleTaskName + " has been trigger !");
        doScheduleTask(className);
    }

    /**
     *
     * doTaskWorker.
     *
     */
    public void doTaskWorker() {
        // taskWorker do work
        Class<?> className = this.getClass();
        String scheduleTaskName = className.getSimpleName();
        log.info("Do TaskWorker ==> " + scheduleTaskName);
        try {
            TimeUnit.SECONDS.sleep(10);
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("End TaskWorker ==> " + scheduleTaskName);
    }
}

** 新增 ScheduleTaskWorkerPrimarySample 範例 於 worker Package

主要展示新增排程任務,

  • triggerScheduleTasks程式無須調整(自動以Class名稱註冊排程),依說明@Scheduled即可。
  • doTaskWorker程式,於 Try {} 端落實做排程任務
  • 指定使用EntityManager 進行資料庫操作,可以試試看 Mybatis應該可以使用。
package tw.lewishome.webapp.base.schedule.worker;

import java.util.List;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import jakarta.persistence.EntityManager;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.schedule.BaseScheduleTask;
import tw.lewishome.webapp.database.primary.entity.SysScheduleLogEntity;

/**
 *
 * ScheduleTaskWorkerPrimarySample 是一個排程任務的範例實作類別,繼承自
 * {@link tw.lewishome.webapp.base.schedule.BaseScheduleTask}。
 * 此類別主要用於示範如何在 Spring 框架下,透過 @Scheduled 註解進行定時任務的執行,
 * 並且指定使用 primaryEntityManager 進行資料庫操作。
 *
 *
 *
 * 主要功能包含:
 * <ul>
 * <li>透過 @Scheduled(cron = "0 0/1 * * * ?") 設定每分鐘執行一次 triggerScheduleTask
 * 方法。</li>
 * <li>triggerScheduleTask 方法會觸發排程任務,並呼叫 doScheduleTask 進行任務處理。</li>
 * <li>doTaskWorker 方法為實際執行任務的範例,包含模擬任務執行時間(休眠 10 秒),可依需求進行資料庫查詢或其他業務邏輯。</li>
 * <li>支援多種 @Scheduled
 * 設定方式(fixedDelay、fixedRate、initialDelay、cron),可依排程需求調整。</li>
 * <li>自動注入 primaryEntityManager,方便進行 JPA 資料庫操作。</li>
 * </ul>
 *
 *
 *
 * 使用說明:
 * <ul>
 * <li>將此類別註冊為 Spring 的 Service。</li>
 * <li>可依需求調整 @Scheduled 註解的排程規則。</li>
 * <li>如需查詢資料庫,可透過 entityManager 執行 JPQL 查詢。</li>
 * </ul>
 *
 *
 *
 * 注意事項:
 * <ul>
 * <li>排程任務執行時,請注意資源管理與例外處理,避免長時間阻塞或資源洩漏。</li>
 * <li>如有多個 EntityManager,請確認 @Qualifier 設定正確。</li>
 * </ul>
 *
 *
 * @author Lewis
 * @since 2024
 */
@Service
@Slf4j
public class ScheduleTaskWorkerPrimarySample extends BaseScheduleTask {
    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * Constructs a new ScheduleTaskWorkerPrimarySample instance.
     * This is the default constructor, implicitly provided by the compiler
     * if no other constructors are defined.
     */
    public ScheduleTaskWorkerPrimarySample() {
        // Constructor body (can be empty)
    }

    /**
     * 指定使用 primaryEntityManager
     */
    @Autowired // 不指定@Qualifier 自動使用 primaryEntityManagerFactory
    private @Qualifier("primaryEntityManager") EntityManager entityManager;

    // @Scheduled的設定方案:
    // @Scheduled(fixedDelay = 5000) // fixedDelay = 5000表示當前方法執行完畢5000ms後,Spring
    // scheduling會再次呼叫該方法
    // public void testFixDelay() {
    // logger.info("===fixedDelay: 第{}次執行方法", fixedDelayCount);
    // }
    //
    // @Scheduled(fixedRate = 5000) // fixedRate = 5000表示當前方法開始執行5000ms後,Spring
    // scheduling會再次呼叫該方法
    // public void testFixedRate() {
    // logger.info("===fixedRate: 第{}次執行方法", fixedRateCount);
    // }
    //
    // @Scheduled(initialDelay = 1000, fixedRate = 5000) // initialDelay =
    // 1000表示延遲1000ms執行第一次任務
    // public void testInitialDelay() {
    // logger.info("===initialDelay: 第{}次執行方法", initialDelayCount);
    // }
    // cron 接受cron 表示式,根據cron表示式確定定時規則
    // @Scheduled(cron = "0 0/10 * * * ?") // 每10分鐘
    // "second, minute, hour, day of month, month, day(s) of week"
    // public void testCron() {
    // logger.info("===Crob: 第{}次執行方法", Cron);
    // }
    /**
     *
     * triggerScheduleTask.
     *
     */
    @Scheduled(cron = "0 0/1 * * * ?") // 每1分鐘執行
    public void triggerScheduleTask() {
        // Class for Schedule Task

        Class<?> className = this.getClass();
        String scheduleTaskName = className.getSimpleName();
        log.info(scheduleTaskName + " has been trigger !");
        doScheduleTask(className);
    }

    /**
     *
     * doTaskWorker.
     *
     */
    // @SuppressWarnings("unchecked")
    @SuppressWarnings("unchecked")
    public void doTaskWorker() {
        // taskworker do work
        Class<?> className = this.getClass();
        String scheduleTaskName = className.getSimpleName();
        log.info("Do TaskWorker ==> " + scheduleTaskName);

        try {
            String hql = "SELECT u from SysScheduleLogEntity u ";
            List<SysScheduleLogEntity> results = entityManager.createQuery(hql).getResultList();
            results.forEach(x -> {
                log.info(x.toString());
            });
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
        log.info("End TaskWorker ==> " + scheduleTaskName);
    }
}


ScheduleTask啟動的畫面

** console log畫面

https://ithelp.ithome.com.tw/upload/images/20251126/20139477wHdAmSz3b0.png

** SysScheduleCtlEntity資料

https://ithelp.ithome.com.tw/upload/images/20251126/20139477MhffBNRsl4.png

** SysScheduleLogEntity資料

https://ithelp.ithome.com.tw/upload/images/20251126/20139477aBGIwM44bn.png


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言